/*
 * Copyright (c) 2009 Nathaniel Houghton <nathan@brainwerk.org>
 *                    Robert Kirchgessner <rkirchge@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for
 * any purpose with or without fee is hereby granted, provided that
 * the above copyright notice and this permission notice appear in all
 * copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
 * AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
 * OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 * PERFORMANCE OF THIS SOFTWARE.
 *
 */

#include <stdint.h>
#include <string.h>
#include <stdio.h>

#include <avr/io.h>
#include <util/delay.h>

#include "nvm.h"
#include "spi.h"
#include "util.h"
#include "lcd.h"
#include "usart.h"

#define nvm_busy() (nvm_status(NVM_READ, 0) & NVM_BUSY_BIT)

#define NVM_SPRL_BIT (1 << 7)
#define NVM_SPM_BIT (1 << 6)
#define NVM_EPE_BIT (1 << 5)
#define NVM_WPP_BIT (1 << 4)
#define NVM_SWP_BIT (1 << 3 | 1 << 2)
#define NVM_WEL_BIT (1 << 1)
#define NVM_BUSY_BIT (1 << 0)

#define NVM_CMD_READ 0x03

#define NVM_CMD_WDISABLE 0x04
#define NVM_CMD_WENABLE 0x06

#define NVM_CMD_STATUS_READ 0x05
#define NVM_CMD_STATUS_WRITE 0x01

#define NVM_CMD_BYTE_PROGRAM 0x02

#define NVM_CMD_ERASE_CHIP 0x60

void
nvm_read(void *vdst, uint32_t src, uint8_t len)
{
	uint8_t out[4];
	uint8_t *dst;

	dst = (uint8_t *) vdst;

	out[0] = NVM_CMD_READ;
	
	src = byte_swap(src);
	memcpy(&out[1], (uint8_t *) &src + 1, sizeof(src) - 1);

	/* select slave */
	SPI_SEL();

	/* send read array command */
	spi_tx(out, sizeof(out));

	spi_rx(dst, len);

	/* deselect slave (terminate read) */
	SPI_DSEL();
}

int
nvm_erase(void)
{
	uint8_t op;

	op = NVM_CMD_ERASE_CHIP;

	nvm_wel(NVM_SET);

	SPI_SEL();

	spi_tx(&op, sizeof(op));

	SPI_DSEL();

	while (nvm_busy());

	if (nvm_status(NVM_READ, 0) & NVM_EPE_BIT)
		fatal("NVM write failed!");

	return 0;
}

uint8_t
nvm_status(uint8_t op, uint8_t status)
{
	if (op == NVM_WRITE) {
		nvm_wel(NVM_SET);
		op = NVM_CMD_STATUS_WRITE;
		SPI_SEL();
		spi_tx(&op, sizeof(op));
		spi_tx(&status, sizeof(status));
	} else {
		op = NVM_CMD_STATUS_READ;
		SPI_SEL();
		spi_tx(&op, sizeof(op));
		spi_rx(&status, sizeof(status));
	}

	SPI_DSEL();

	return status;
}

/*
 * Must be called before status register can be written, and also
 * before a byte or sequential write.
 */
void
nvm_wel(uint8_t op)
{
	while (nvm_busy());

	if (op == NVM_SET)
		op = NVM_CMD_WENABLE;
	else
		op = NVM_CMD_WDISABLE;

	SPI_SEL();
	
	spi_tx(&op, sizeof(op));

	SPI_DSEL();
}

void
nvm_write(uint32_t dst, void *vsrc, uint16_t len)
{
	uint8_t out[4];
	uint32_t be_dst;
	uint16_t chunk_size;
	uint8_t page_offset;
	uint8_t *src;

	src = (uint8_t *) vsrc;
	
	out[0] = NVM_CMD_BYTE_PROGRAM;
	
	/*
	 * Calculate page offset. Page offset is 0 for 2nd, 3rd, etc
	 * write.
	 */
	page_offset = dst % NVM_PAGE_SIZE;
	chunk_size = NVM_PAGE_SIZE - page_offset;

	while (len != 0) {
		be_dst = byte_swap(dst);
		memcpy(&out[1], (uint8_t *) &be_dst + 1, sizeof(be_dst) - 1);

		if (chunk_size > len)
			chunk_size = len;

#if 0
		char buf[128];
		int i = 0;
		_delay_ms(1000);

		lcd_clrscr();
		lcd_home(0);
		sprintf(buf, "a: %08lX o: %02X", be_dst, page_offset);
		lcd_print(buf);
		sprintf(buf, "cs: %d n: %d", chunk_size, ++i);
		lcd_home(1);
		lcd_print(buf);
		
		_delay_ms(1000);
#endif

		/* enable writing (disabled after each write command) */
		nvm_wel(NVM_SET);
#if 1

		SPI_SEL();

		spi_tx(out, sizeof(out));
		spi_tx(src, chunk_size);

		SPI_DSEL();
#endif

		len -= chunk_size;
		dst += chunk_size;
		src += chunk_size;
		chunk_size = NVM_PAGE_SIZE;
		page_offset = 0;
		
		/* wait for write to finish */
		while (nvm_busy());
	
		/* programming failure */
		if (nvm_status(NVM_READ, 0) & NVM_EPE_BIT)
			fatal("NVM write failed!");
	}
}
